/**
 * Copyright (c) 2012-2013 Nokia Corporation. All rights reserved.
 * Nokia and Nokia Connecting People are registered trademarks of Nokia Corporation.
 * Oracle and Java are trademarks or registered trademarks of Oracle and/or its
 * affiliates. Other product and company names mentioned herein may be trademarks
 * or trade names of their respective owners.
 * See LICENSE.TXT for license information.
 */

package com.nokia.example.amaze.ui;

import javax.microedition.m3g.Camera;
import javax.microedition.m3g.Transform;
import javax.microedition.m3g.World;

/**
 * Class for pause and transition camera animations.
 */
public class CameraAnimator {
    // Constants
    private static final float NUM_OF_TRANSITION_STEPS = 30;
    private static final float MIN_TRANSITION_STEP = 0.01f;
    private static final float PAUSE_ANIMATION_ROTATION_SPEED = 0.2f;
    
    // Animation types
    public static final int PAUSE_ANIMATION = 0;
    public static final int TRANSITION_ANIMATION_TO_POV = 1;
    public static final int TRANSITION_ANIMATION_TO_TOP = 2;
    public static final int LEVEL_RESET_ANIMATION_STEP = 3;

    // Members
    private Listener _listener = null;
    private World _world = null;
    private Camera _camera = null;
    private Transform _currentTransform = null;
    private Transform _targetTransform = null;
    private float[] _currentMatrix = null;
    private float[] _targetMatrix = null;
    private float[] _transitionMatrix = null;
    private float[] _currentOrientation = null;
    private float[] _targetOrientation = null;
    private int _animationType = -1;
    private int _stepCount = 0;
    private boolean _running = false;

    /**
     * Constructor.
     * @param listener The listener which is the maze canvas.
     */
    public CameraAnimator(Listener listener, World world, Camera camera) {
        if (listener == null || world == null || camera == null) {
            throw new IllegalArgumentException("None of the arguments can be null!");
        }
        
        _listener = listener;
        _world = world;
        _camera = camera;
    }

    /**
     * Starts the animation of the given type.
     * 
     * Note that after calling this method only "_currentTransform" of the
     * members is guaranteed not to be null. If the given target transform is
     * not null, the following memebers are guaranteed not be null:
     * - _currentTransform,
     * - _targetTransform,
     * - _targetMatrix and
     * - _currentMatrix.
     *
     * @param type The animation type.
     * @param targetTransform The target transform. Can be null.
     * @param targetOrientation The target orientation. Can be null.
     */
    public void startAnimation(int type,
                               Transform targetTransform,
                               float[] targetOrientation)
    {
        System.out.println("CameraAnimator::startAnimation(): " + type);
        
        if (_running) {
            releaseResources();
            _running = false;
        }
        
        _animationType = type;
        _stepCount = 0;
        
        if (targetTransform != null) {
            _targetTransform = targetTransform;
            _targetMatrix = new float[16];
            _targetTransform.get(_targetMatrix);
        } 
        
        _currentTransform = new Transform();
        Camera activeCamera = _world.getActiveCamera();
        
        if (activeCamera == null) {
            // No active camera set, do set it now
            System.out.println("CameraAnimator::startAnimation(): No active camera set.");
            _world.setActiveCamera(_camera);
            _camera.getTransform(_currentTransform);
        }
        else {
            activeCamera.getTransform(_currentTransform);
            
            if (_camera != activeCamera) {
                System.out.println("CameraAnimator::startAnimation(): Switching the camera.");
                _camera.setTransform(_currentTransform);
                _world.setActiveCamera(_camera);
            }
        }
        
        if (targetOrientation != null) {
            _targetOrientation = targetOrientation;
            _currentOrientation = new float[4];
            _camera.getOrientation(_currentOrientation);
        }
        
        if (targetTransform != null) {
            _currentMatrix = new float[16];
            _currentTransform.get(_currentMatrix);
            
            _transitionMatrix = calculateTransitionMatrix(_currentMatrix,
                                                          _targetMatrix);
        }
        
        _running = true;
    }

    /**
     * For convenience.
     * @param type
     * @param targetCamera
     */
    public void startAnimation(int type, Camera targetCamera) {
        if (targetCamera == null) {
            return;
        }
        
        Transform transform = new Transform();
        targetCamera.getTransform(transform);
        float[] orientation = new float[4];
        targetCamera.getOrientation(orientation);
        startAnimation(type, transform, orientation);
    }

    /**
     * For convenience.
     * @param type
     */
    public void startAnimation(int type) {
        startAnimation(type, null, null);
    }

    /**
     * Stops the animation.
     */
    public void stopAnimation() {
        System.out.println("CameraAnimator::stopAnimation()");
        releaseResources();
        _running = false;
        _listener.onAnimationFinished(_animationType);
    }

    /** 
     * @return True if the animator running, false otherwise.
     */
    public final boolean running() {
        return _running;
    }

    /** 
     * @return The type of the current animation.
     */
    public final int animationType() {
        return _animationType;
    }

    /**
     * Takes one update step in the animation.
     * @param ticks The milliseconds since last time the method was called.
     */
    public final void update(final int ticks) {
        final float coefficient = (float)ticks / MazeCanvas.TICKS_COEFFICIENT;
        
        if (_targetTransform != null) {
            if (!takeTransitionStepTowardsTarget(coefficient)) {
                System.out.println("CameraAnimator::update(): No more transition steps required.");
                
                if (_animationType == TRANSITION_ANIMATION_TO_POV
                    || _animationType == TRANSITION_ANIMATION_TO_TOP)
                {
                    // We're done here
                    _running = false;
                    stopAnimation();
                }
                else {
                    releaseResources();
                }
            }
            
            if (_currentTransform != null) {
                _camera.setTransform(_currentTransform);
            }
            
            if (_targetOrientation != null) {
                _camera.setOrientation(_currentOrientation[0],
                                       _currentOrientation[1],
                                       _currentOrientation[2],
                                       _currentOrientation[3]);
            }
        }
        else if (_animationType == PAUSE_ANIMATION) {
            _camera.postRotate(PAUSE_ANIMATION_ROTATION_SPEED * coefficient, 0f, 1f, 0f);
        }
        else if (_animationType == LEVEL_RESET_ANIMATION_STEP) {
            _camera.translate(0f, 12.0f * coefficient, 12.0f * coefficient);
            _stepCount++;
            
            if (_stepCount > 60) {
                stopAnimation();
            }
        }
        else {
            stopAnimation();
        }
    }

    /**
     * Prints the matrix of the given transform. For debugging purposes.
     * @param transform The transform to print.
     */
    public static final void printTransform(Transform transform) {
        if (transform == null) {
            return;
        }
        
        final float[] matrix = new float[16];
        transform.get(matrix);
        
        for (int i = 0; i < 16; ++i) {
            if ((i + 1) % 4 == 0) {
                System.out.println(matrix[i]);
            }
            else {
                System.out.print(matrix[i] + "\t");
            }
        }
    }

    /**
     * Prints the values of the properties of the given camera.
     * @param camera The camera of which values to print.
     */
    public static final void printCameraValues(Camera camera) {
        if (camera != null) {
            Transform transform = new Transform();
            camera.getTransform(transform);
            printTransform(transform);
            
            // Print orientation
            final float[] orientation = new float[4];
            camera.getOrientation(orientation);
            System.out.print("Orientation: [");
            System.out.println(orientation[0] + ", "
                               + orientation[1] + ", "
                               + orientation[2] + ", "
                               + orientation[3] + "]");
        }
    }

    /**
     * Modifies the value of "current" by the value of "diff" towards "target".
     * @param current The initial value.
     * @param target The target value.
     * @param diff The difference.
     * @return The modified value.
     */
    private final float newValue(float current, float target, float diff) {
        if (diff < 0) {
            diff = Math.abs(diff);
        }
        
        if (Math.abs(current - target) < diff) {
            return target;
        }

        if (target > current) {
            return current + diff;
        }
        
        return current - diff;
    }

    /**
     * Takes a step towards the target camera transition.
     * @param coefficient Defines the relative size of the step.
     * @return True if the matrix was modified, false otherwise.
     */
    private final boolean takeTransitionStepTowardsTarget(final float coefficient) {
        if (_currentMatrix == null || _targetMatrix == null) {
            System.out.println("CameraAnimator::takeTransitionStepTowardsTarget(): Null matrices!");
            return false;
        }
        
        final int matrixLength = _currentMatrix.length;
        final int maxChangesCount = (_targetOrientation == null)
                ? matrixLength : _transitionMatrix.length;
        int noChangesCount = 0; 
        
        for (int i = 0; i < matrixLength; ++i) {
            if (_currentMatrix[i] == _targetMatrix[i]) {
                noChangesCount++;
                continue;
            }
            
            _currentMatrix[i] = newValue(_currentMatrix[i],
                                         _targetMatrix[i],
                                         _transitionMatrix[i] * coefficient);
        }
        
        if (_currentOrientation != null && _targetOrientation != null) {
            for (int i = 0; i < 4; ++i) {
                if (_currentOrientation[i] == _targetOrientation[i]) {
                    noChangesCount++;
                    continue;
                }
                
                _currentOrientation[i] =
                        newValue(_currentOrientation[i],
                                 _targetOrientation[i],
                                 _transitionMatrix[i + matrixLength] * coefficient);
            }
        }
        
        _currentTransform.set(_currentMatrix);
        return (noChangesCount != maxChangesCount);
    }

    /**
     * Creates the transition matrix whose values represent a single transition
     * step (delta) from the initial matrix to the target matrix.
     * @param from The initial matrix.
     * @param to The target matrix.
     * @return The transition (step delta) matrix.
     */
    private float[] calculateTransitionMatrix(float[] from, float[] to) {
        if (from == null || to == null || from.length != to.length){
            return null;
        }
        
        final int length = from.length;
        float[] transitionMatrix = new float[length + 4];
        
        for (int i = 0; i < length; ++i) {
            if (to[i] == from[i]) {
                continue;
            }
            
            transitionMatrix[i] = (to[i] - from[i]) / NUM_OF_TRANSITION_STEPS;
            
            if (transitionMatrix[i] == 0) {
                transitionMatrix[i] = MIN_TRANSITION_STEP;
            }
        }
        
        if (_targetOrientation != null) {        
            // Calculate the transition steps for the orientation
            for (int i = 0; i < 4; ++i) {
                transitionMatrix[i + length] =
                        (_targetOrientation[i] - _currentOrientation[i])
                        / NUM_OF_TRANSITION_STEPS;
                
                if (transitionMatrix[i + length] == 0) {
                    transitionMatrix[i + length] = MIN_TRANSITION_STEP;
                }
            }
        }
        
        return transitionMatrix;
    }

    /**
     * Releases resources.
     */
    private void releaseResources() {
        _currentTransform = null;
        _targetTransform = null;
        _targetMatrix = null;
        _currentMatrix = null;
        _currentOrientation = null;
        _targetOrientation = null;
        _transitionMatrix = null;
    }

    /**
     * Listener interface for events of this class.
     */
    public interface Listener {
        /**
         * Called when an animation is finished.
         * @param animationType The type of the animation which was finished.
         */
        void onAnimationFinished(int animationType);
    }
}
